﻿using UnityEngine;
using System.Collections;
using System.Text;
using System.Collections.Generic;
using ZXthex.Char;

/// <summary>
/// 参考自HTML标准
/// https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model
/// </summary>
class HtmlDocumentBuilder
{
    enum TokenizationState
    {
        Data,
        //RCDATA,
        RAWTEXT,
        //ScriptData,
        PLAINTEXT,
        TagOpen,
        EndTagOpen,
        TagName,
        //RCDATA_less_than_sign,
        //RCDATA_end_tag_open,
        //RCDATA_end_tag_name,
        BeforeAttributeName,
        AttributeName,
        AfterAttributeName,
        BeforeAttributeValue,
        AttributeValue_double_quoted,
        AttributeValue_single_quoted,
        AttributeValue_unquoted,
        AfterAttributeValue_quoted,
        SelfClosingStartTag,
        MarkupDeclarationOpen,
        CommentStart,
        CommentStartDash,
        Comment,
        Comment_less_than_sign,
        Comment_less_than_sign_bang,
        Comment_less_than_sign_bang_dash,
        Comment_less_than_sign_bang_dash_dash,
        CommentEndDash,
        CommentEnd,
        CommentEndBang,
        DOCTYPE,

        //To be simple, ignore following states...
        //Before_DOCTYPE_name,
        //DOCTYPE_name,
        //After_DCOTYPE_name,
        //After_DOCTYPE_public_keyword,


    }

    enum BuildTokenType
    {
        Characters,
        StartTag,
        EndTag,
        EOF,
        Comment,
        DOCTYPE,
    }

    class BuildToken
    {
        public BuildTokenType type { get; private set; }
        public BuildToken(BuildTokenType type)
        {
            this.type = type;
            sBuilder = new StringBuilder(8);
            attributes = new List<BuildAttribute>();
        }

        private StringBuilder sBuilder;
        public string text { get { return sBuilder.ToString(); } }
        public void Append(char c)
        {
            sBuilder.Append(c);
        }

        public bool SelfClosingFlag { get; set; }

        public List<BuildAttribute> attributes { get; private set; }

        public void NewAttribute()
        {
            attributes.Add(new BuildAttribute());
        }

        public BuildAttribute currentAttribute
        {
            get
            {
                return attributes[attributes.Count - 1];
            }
        }
    }

    class BuildAttribute
    {
        StringBuilder nameBuilder;
        StringBuilder valueBuilder;

        public BuildAttribute()
        {
            nameBuilder = new StringBuilder(8);
            valueBuilder = new StringBuilder(8);
        }

        public void AppendName(char c)
        {
            nameBuilder.Append(c);
        }

        public void AppendValue(char c)
        {
            valueBuilder.Append(c);
        }

        public KeyValuePair ConvertToKeyValuePair()
        {
            return new KeyValuePair()
            {
                key = nameBuilder.ToString(),
                value = valueBuilder.ToString()
            };
        }
    }

    static readonly HtmlDocumentBuilder _default = new HtmlDocumentBuilder();
    public static HtmlDocumentBuilder Default{ get { return _default; } }
    

    public void LoadHtml(NextChars reader, out ReadOnlyHtmlNode root, out ReadOnlyHtmlNode head, out ReadOnlyHtmlNode body)
    {

        CheckTrueEncoding(reader);

        PreprocessInputStream(reader);

        var tokens = DoTokenization(reader);
       
        //for (int i = 0; i < tokens.Count; i++)
        //{
        //    if (tokens[i].type == BuildTokenType.Characters && tokens[i].text.Length == 0) continue;
        //    //Debug.Log("token " + tokens[i].type + " " + tokens[i].text + " "+(tokens[i].SelfClosingFlag?"selfclosed":""));
        //}

        DoTreeConstruction(tokens, out root, out head, out body);
    }

    void CheckTrueEncoding(NextChars reader)
    {

    }

    /// <summary>
    /// Replace \r\n to \n, then replace \r to \n
    /// </summary>
    void PreprocessInputStream(NextChars reader)
    {
        reader.PreprocessChars();
    }

    List<BuildToken> DoTokenization(NextChars reader)
    {
        reader.ResetPosition();

        List<BuildToken> tokens = new List<BuildToken>();

        BuildToken token = new BuildToken(BuildTokenType.Characters);
        TokenizationState state = TokenizationState.Data;

        char c = '\0';

        bool EOF = false;

        int line = 1;

        bool should_reconsume = false;

        while (true)
        {
            if (!should_reconsume)
            {
                if (c == '\n')
                {
                    line++;
                }
                if (reader.CanGoNext)
                {
                    c = reader.NextChar();
                }
                else
                {
                    EOF = true;
                }
            }
            else
            {
                should_reconsume = false;
            }
            
            switch (state)
            {
                case TokenizationState.Data:
                    #region Data State
                    if (EOF)
                    {
                        tokens.Add(token);
                    }
                    else if (c == '<')
                    {
                        state = TokenizationState.TagOpen;
                    }
                    else
                    {
                        token.Append(c);
                    }
                    #endregion
                    break;
                case TokenizationState.TagOpen:
                    #region Tag Open
                    if (c == '!')
                    {
                        state = TokenizationState.MarkupDeclarationOpen;
                    }
                    else if (c == '/')
                    {
                        state = TokenizationState.EndTagOpen;
                    }
                    else if (c.IsAlpha())
                    {
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.StartTag);
                        should_reconsume = true;
                        state = TokenizationState.TagName;
                    }
                    else
                    {
                        ParseError("invalid first character of tag name", line);
                    }
                    #endregion
                    break;
                case TokenizationState.EndTagOpen:
                    #region End Tag Open
                    if (c.IsAlpha())
                    {
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.EndTag);
                        should_reconsume = true;
                        state = TokenizationState.TagName;
                    }
                    else if (c == '>')
                    {
                        ParseError("missing end tag name" , line);
                    }
                    else
                    {
                        ParseError("invalid first character of tag name", line);
                    }
                    #endregion
                    break;
                case TokenizationState.TagName:
                    #region Tag Name
                    if (c.IsIgnoreChar())
                    {
                        state = TokenizationState.BeforeAttributeName;
                    }
                    else if (c == '/')
                    {
                        state = TokenizationState.SelfClosingStartTag;
                    }
                    else if (c == '>')
                    {
                        state = TokenizationState.Data;

                        tokens.Add(token);

                        token = new BuildToken(BuildTokenType.Characters);
                    }
                    else if (char.IsUpper(c))
                    {
                        token.Append(char.ToLower(c));
                    }
                    else
                    {
                        token.Append(c);
                    }
                    #endregion
                    break;
                case TokenizationState.BeforeAttributeName:
                    #region Before Attribute Name
                    if (c.IsIgnoreChar())
                    {
                        //ignore
                    }
                    else if (c == '/' || c == '>')
                    {
                        state = TokenizationState.AfterAttributeName;
                        should_reconsume = true;
                    }
                    else if (c == '=')
                    {
                        ParseError("unexpected equals sign before attribute name", line);
                    }
                    else
                    {
                        token.NewAttribute();
                        state = TokenizationState.AttributeName;
                        should_reconsume = true;
                    }
                    #endregion
                    break;
                case TokenizationState.AttributeName:
                    #region Attribute Name
                    if (c.IsIgnoreChar() ||  c == '/' || c == '>')
                    {
                        should_reconsume = true;
                        state = TokenizationState.AfterAttributeName;
                    }
                    else if (c == '=')
                    {
                        state = TokenizationState.BeforeAttributeValue;
                    }
                    else if (char.IsUpper(c))
                    {
                        token.currentAttribute.AppendName(char.ToLower(c));
                    }
                    else
                    {
                        token.currentAttribute.AppendName(c);
                    }
                    #endregion
                    break;
                case TokenizationState.AfterAttributeName:
                    #region After Attribute Name
                    if (c.IsIgnoreChar())
                    {
                        //ignore
                    }
                    else if (c == '/')
                    {
                        state = TokenizationState.SelfClosingStartTag;
                    }
                    else if (c == '=')
                    {
                        state = TokenizationState.BeforeAttributeValue;
                    }
                    else if (c == '>')
                    {
                        state = TokenizationState.Data;
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                    }
                    else
                    {
                        token.NewAttribute();
                        should_reconsume = true;
                        state = TokenizationState.AttributeName;
                    }
                    #endregion
                    break;
                case TokenizationState.BeforeAttributeValue:
                    #region Before Attribute Value
                    if (c.IsIgnoreChar())
                    {
                        //ignore
                    }
                    else if (c == '\"')
                    {
                        state = TokenizationState.AttributeValue_double_quoted;
                    }
                    else if (c == '\'')
                    {
                        state = TokenizationState.AttributeValue_single_quoted;
                    }
                    else if (c == '>')
                    {
                        ParseError("missing attribute value", line);
                    }
                    else
                    {
                        should_reconsume = true;
                        state = TokenizationState.AttributeValue_unquoted;
                    }
                    #endregion
                    break;
                case TokenizationState.AttributeValue_double_quoted:
                    if (c == '\"')
                    {
                        state = TokenizationState.AfterAttributeValue_quoted;
                    }
                    else
                    {
                        token.currentAttribute.AppendValue(c);
                    }
                    break;
                case TokenizationState.AttributeValue_single_quoted:
                    if (c == '\'')
                    {
                        state = TokenizationState.AfterAttributeValue_quoted;
                    }
                    else
                    {
                        token.currentAttribute.AppendValue(c);
                    }
                    break;
                case TokenizationState.AttributeValue_unquoted:
                    if (c.IsIgnoreChar())
                    {
                        state = TokenizationState.BeforeAttributeName;
                    }
                    else if (c == '>')
                    {
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                        state = TokenizationState.Data;
                    }
                    else if (c == '\"' || c == '\'' || c == '<' || c == '=' || c == '`')
                    {
                        ParseError("unexpected character in unquoted attribute value", line);
                    }
                    else
                    {
                        token.currentAttribute.AppendValue(c);
                    }
                    break;
                case TokenizationState.AfterAttributeValue_quoted:
                    if (c.IsIgnoreChar())
                    {
                        state = TokenizationState.BeforeAttributeName;
                    }
                    else if (c == '/')
                    {
                        state = TokenizationState.SelfClosingStartTag;
                    }
                    else if (c == '>')
                    {
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                        state = TokenizationState.Data;
                    }
                    else
                    {
                        ParseError("missing whitespace between attributes", line);
                    }

                    break;
                case TokenizationState.SelfClosingStartTag:
                    if (c == '>')
                    {
                        token.SelfClosingFlag = true;
                        state = TokenizationState.Data;
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                    }
                    else
                    {
                        ParseError("unexpected solidus in tag", line);
                    }
                    break;
                case TokenizationState.MarkupDeclarationOpen:
                    if (c + reader.PeekFewChars(1) == "--")
                    {
                        state = TokenizationState.CommentStart;
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Comment);
                        reader.NextChar();
                        reader.NextChar();
                    }
                    else if ((c + reader.PeekFewChars(6)).ToUpper() == "DOCTYPE")
                    {
                        state = TokenizationState.DOCTYPE;
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.DOCTYPE);
                        for (int i = 0; i < 7; i++) reader.NextChar();
                    }
                    else
                    {
                        ParseError("incorrectly opened comment", line);
                    }
                    break;
                case TokenizationState.CommentStart:
                    if (c == '-')
                    {
                        state = TokenizationState.CommentStartDash;
                    }
                    else if (c == '>')
                    {
                        ParseError("abrupt closing of empy comment");
                    }
                    else
                    {
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.CommentStartDash:
                    if (c == '-')
                    {
                        state = TokenizationState.CommentEnd;
                    }
                    else if (c == '>')
                    {
                        ParseError("abrupt closing of empty comment", line);
                    }
                    else
                    {
                        token.Append('-');
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.Comment:
                    if (c == '<')
                    {
                        token.Append(c);
                        state = TokenizationState.Comment_less_than_sign;
                    }
                    else if (c == '-')
                    {
                        state = TokenizationState.CommentEndDash;
                    }
                    else
                    {
                        token.Append(c);
                    }
                    break;
                case TokenizationState.Comment_less_than_sign:
                    if (c == '!')
                    {
                        token.Append(c);
                        state = TokenizationState.Comment_less_than_sign_bang;
                    }
                    else if (c == '<')
                    {
                        token.Append(c);
                    }
                    else
                    {
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.Comment_less_than_sign_bang:
                    if (c == '-')
                    {
                        state = TokenizationState.Comment_less_than_sign_bang_dash;
                    }
                    else
                    {
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.Comment_less_than_sign_bang_dash:
                    if (c == '-')
                    {
                        state = TokenizationState.Comment_less_than_sign_bang_dash_dash;
                    }
                    else
                    {
                        should_reconsume = true;
                        state = TokenizationState.CommentEndDash;
                    }
                    break;
                case TokenizationState.Comment_less_than_sign_bang_dash_dash:
                    if (c == '>' || EOF)
                    {
                        should_reconsume = true;
                        state = TokenizationState.CommentEnd;
                    }
                    else
                    {
                        ParseError("nested comment", line);
                    }
                    break;
                case TokenizationState.CommentEndDash:
                    if (c == '-')
                    {
                        state = TokenizationState.CommentEnd;
                    }
                    else
                    {
                        state = TokenizationState.Comment;
                        token.Append('-');
                        should_reconsume = true;
                    }
                    break;
                case TokenizationState.CommentEnd:
                    if (c == '>')
                    {
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                        state = TokenizationState.Data;
                    }
                    else if (c == '!')
                    {
                        state = TokenizationState.CommentEndBang;
                    }
                    else if (c == '-')
                    {
                        token.Append('-');
                    }
                    else
                    {
                        token.Append('-');
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.CommentEndBang:
                    if (c == '-')
                    {
                        token.Append('-');
                        token.Append('-');
                        token.Append('!');
                        state = TokenizationState.CommentEndDash;
                    }
                    else if (c == '>')
                    {
                        ParseError("incorrectly closed comment", line);
                    }
                    else
                    {
                        token.Append('-');
                        token.Append('-');
                        token.Append('!');
                        should_reconsume = true;
                        state = TokenizationState.Comment;
                    }
                    break;
                case TokenizationState.DOCTYPE:
                    if (c == '>')
                    {
                        state = TokenizationState.Data;
                        tokens.Add(token);
                        token = new BuildToken(BuildTokenType.Characters);
                    }
                    break;
                default:
                    break;
            }

            if (EOF)
            {
                tokens.Add(new BuildToken(BuildTokenType.EOF));
                break;
            }

        }

        return tokens;
    }

    void DoTreeConstruction(List<BuildToken> tokens, out ReadOnlyHtmlNode root, out ReadOnlyHtmlNode head, out ReadOnlyHtmlNode body)
    {

        head = null;
        body = null;

        Stack<RawNode> nodes = new Stack<RawNode>(16);
        RawNode raw_root = null;
        bool justEnd = true;
        for (int i = 0; i < tokens.Count; i++)
        {
            if (tokens[i].type == BuildTokenType.DOCTYPE
                || tokens[i].type == BuildTokenType.Comment)
                continue;

            if (tokens[i].type == BuildTokenType.StartTag)
            {
                justEnd = IsVoidElement(tokens[i].text);
                var node = new RawNode();
                node.Name = tokens[i].text;
                node.attributes = new List<KeyValuePair>(tokens[i].attributes.Count);
                for (int j = 0; j < tokens[i].attributes.Count; j++)
                {
                    node.attributes.Add(tokens[i].attributes[j].ConvertToKeyValuePair());
                }
                if (nodes.Count == 0)
                {
                    raw_root = node;
                    nodes.Push(node);
                }
                else
                {
                    node.Parent = nodes.Peek();
                    nodes.Peek().childs.Add(node);
                    if (tokens[i].SelfClosingFlag || IsVoidElement(tokens[i].text))
                    {

                    }
                    else
                    {
                        nodes.Push(node);
                    }
                }
            }
            else if (tokens[i].type == BuildTokenType.EndTag)
            {
                justEnd = true;
                nodes.Pop();
            }
            else if (tokens[i].type == BuildTokenType.Characters)
            {
                if (justEnd && nodes.Count > 0)
                {
                    var node = new RawNode();
                    node.Name = "normal text";
                    node.attributes = new List<KeyValuePair>(0);
                    node.InnerText = tokens[i].text.Trim();
                    Debug.Log(node.InnerText);
                    if (nodes.Count == 0)
                    {
                        
                    }
                    else
                    {
                        node.Parent = nodes.Peek();
                        nodes.Peek().childs.Add(node);
                        
                    }

                }
                else
                if (nodes.Count > 0)
                {
                    nodes.Peek().InnerText = tokens[i].text.Trim();
                }
            }
        }

        var hn = ReadOnlyHtmlNode.FromRawNodeRoot(raw_root);

        root = hn;
        for (int i = 0; i < root.ChildCount; i++)
        {
            if (head == null && root.GetChild(i).Name.ToLower() == "head")
            {
                head = root.GetChild(i);
            }
            if (body == null && root.GetChild(i).Name.ToLower() == "body")
            {
                body = root.GetChild(i);
            }
        }

    }


    static readonly string[] voids = { "area", "base", "br", "col", "embed",
        "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", };
    bool IsVoidElement(string name)
    {
        return System.Array.IndexOf(voids, name) >= 0;
    }

    void PrintTree(ReadOnlyHtmlNode node, int level=0)
    {
        Debug.Log("".PadRight(level*2, '-')+ node.Name);
        for (int i = 0; i < node.ChildCount; i++)
        {
            PrintTree(node.GetChild(i), level + 1);
        }
    }
    

    void ParseError(string error_name, int line = -1)
    {
        if (error_name.Length > 1)
            error_name = char.ToUpper(error_name[0]) + error_name.Substring(1);

        if (line >= 0)
            throw new System.FormatException(error_name + " parse error at line " + line);
        else
            throw new System.FormatException(error_name + " parse error");
    }

}

namespace ZXthex.Char
{
    public static class CharHelper
    {
        public static bool IsAlpha(this char c)
        {
            return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
        }

        public static bool IsIgnoreChar(this char c)
        {
            return c == '\t' || c == '\n' || c == '\f' || c == ' ';
        }

        public static bool IsIdentifier(this char c)
        {
            return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
                || c >= '0' && c <= '9' || c=='-' || c == '_';
        }

        public static bool IsPropertyValue(this char c)
        {
            return IsIdentifier(c) || c == '#' || c == '%';
        }
    }
}
